框架调研

由于公司核心服务均在阿里生态,故以适配阿里云百炼为基准做框架调研
核心业务是通用推理+视觉理解推理

dashscope-sdk-java

  • 阿里百炼SDK
  • 这个sdk更倾向于服务于处理非通用AI,比如AI语音生成,图像成生成这些
    • 官方文档里通用文字推理及视觉理解类的java示例均使用OpenAI SDK
  • 由于官方文档示例比较少,对于深度思考/流式输出/多模态 等都没有比较完整的示例

OpenAI SDK

  • OpenAI 官方SDK,阿里大部分通用模型支持走baseUrl使用
  • 默认使用okhttp做客户端
  • 由于原代码是kt写的,构建消息及模态消息需要套几层build
  • 没有ChatMemory相关框架及抽象,需要自行实现

Spring AI Alibaba

  • 个人理解为阿里ai的Spring AI 封装,内核还是dashscope
  • 官网: https://java2ai.com
  • Spring AI中文文档: https://docs.springframework.org.cn/spring-ai
  • 测试文本推理没有大问题,但视觉理解按官方文档调用报错
  • 并且不知道是不是版本原因,官方文档很多示例无法运行,发布了1.0.0.3版本,但没有对应文档

LangChain4j

  • 推荐
  • LangChain框架的Java版本
  • 由于原框架是python所以java版本的api也比较简洁
  • 比如写个ImageContent在大部分框架都需要套几层build或套几层方法,在LangChain里只需要
	  userMsgBuilder.addContent(ImageContent.from(url));
  • 有ChatMemory相关抽象,可实现ChatMemoryStore,ChatMemoryProvider来做基于窗口或tokens的db/redis持久化记忆
  • 提供高层次的AiServices封装,并且对Message模板做了注解抽象,但不支持多模态
  • 提供了PromptTemplate封装,用于处理带参的Prompt工程
  • 使用part流的方式提供流式输出,相对OutputStream更实用

LangChain4j 使用

引入

  • 没有编排等高级需求的话仅需要引入主包
	<dependency>
            <groupId>dev.langchain4j</groupId>
            <artifactId>langchain4j-open-ai</artifactId>
            <version>1.5.0</version>
        </dependency>

Chat

  • 入参
    public class AiChatRequest {
      @Schema(description = "聊天会话id")
      private String sessionId;
    
      @Schema(description = "图片url列表")
      private List<String> imgUrls;
    
      @Schema(description = "文本内容")
      private String text;
    }
    
  • 实始化及不带聊天记忆的chat
    	 @PostConstruct
      private void init(){
          this.chatModel = OpenAiChatModel.builder()
                  .baseUrl(baseUrl).apiKey(apiKey)
                  .modelName(defaultModelName)
                  .temperature(0.6) //AI做题建议用低温度
                  .logRequests(logRequests)
                  .returnThinking(false)
                  .timeout(Duration.ofSeconds(20))
                  .build();
      }
    
      //不带聊天记忆的chat
      public String chat(AiChatRequest request){
          var userMsg = genUserMsg(request.getImgUrls(),request.getText());
          var useVlModel = CollectionUtil.isNotEmpty(request.getImgUrls());
          var chatRequest = genChatRequest(userMsg,useVlModel);
          var chatResp =  chatModel.chat(chatRequest);
          var aiMsg = chatResp.aiMessage();
          return aiMsg.text();
      }
    	//生成userMessage
    	private UserMessage genUserMsg(List<String> imageUrls, String text){
          if(CollectionUtil.isEmpty(imageUrls)){
              return UserMessage.from(text);
          }else {
              var msgBuilder =  UserMessage.builder();
              imageUrls.forEach(url -> {
                  if(StringUtils.isNotBlank(url))
                      msgBuilder.addContent(ImageContent.from(url));
              });
              if(StringUtils.isNotBlank(text))
                  msgBuilder.addContent(TextContent.from(text));
              return msgBuilder.build();
          }
      }
    	//生成chatRequest
    	 private ChatRequest genChatRequest(UserMessage userMessage,Boolean useVlModel){
          var requestBuilder = ChatRequest.builder();
          //关闭思考
          var parameters = OpenAiChatRequestParameters.builder().customParameters(Map.of("enable_thinking", false)).build();
    		 //带图片的消息使用视觉理解模型
          if(useVlModel)
              return requestBuilder.modelName(vlModelName).parameters(parameters).messages(userMessage).build();
          return requestBuilder.messages(userMessage).parameters(parameters).build();
      }
    

ChatMemory

  • 创建基于redis的memoryStore
    static class RedisPersistentChatMemoryStore implements ChatMemoryStore {
          private StringRedisTemplate redisTemplate;
          private int expireSeconds;
          private String keyPrefix = "chat_memory:";
    
          private RedisPersistentChatMemoryStore(){}
    
          public RedisPersistentChatMemoryStore(StringRedisTemplate redisTemplate, int expireSeconds) {
              this.redisTemplate = redisTemplate;
              this.expireSeconds = expireSeconds;
          }
    
          public RedisPersistentChatMemoryStore(StringRedisTemplate redisTemplate,String keyPrefix ,int expireSeconds) {
              this.redisTemplate = redisTemplate;
              this.expireSeconds = expireSeconds;
              this.keyPrefix = keyPrefix;
          }
    
    
          @Override
          public List<ChatMessage> getMessages(Object memoryId) {
              var key = keyPrefix + memoryId.toString();
              var json = redisTemplate.opsForValue().get(key);
              if(Objects.isNull(json))
                  return List.of();
              return ChatMessageDeserializer.messagesFromJson(json);
          }
    
          @Override
          public void updateMessages(Object memoryId, List<ChatMessage> list) {
              var key = keyPrefix + memoryId.toString();
              ChatMessageSerializer.messagesToJson(list);
              //每次更新都更新过期时间,并且无需处理消息数量,消息数量由上层MessageWindowChatMemory或TokenWindowChatMemory 窗口处理
              //并且上层会一直保留第一个系统消息
              redisTemplate.opsForValue().set(key,ChatMessageSerializer.messagesToJson(list), Duration.ofSeconds(expireSeconds));
          }
    
    
          @Override
          public void deleteMessages(Object memoryId) {
              var key = keyPrefix + memoryId.toString();
              redisTemplate.expire(key,Duration.ofSeconds(0));
          }
      }
    
  • 初始化reidsChatMemory
this.chatMemoryStore = new RedisPersistentChatMemoryStore(redisTemplate, 60*60);
  • 带聊天记忆的chat,注意sessionId使用,系统消息不会从消息窗口里删除
   private ChatMemory getChatMemory(String sessionId){
        return MessageWindowChatMemory.builder()
                .chatMemoryStore(chatMemoryStore)
                .maxMessages(maxStoreMessages)
                .id(sessionId)
                .build();
    }
	//带聊天记忆的chat,输出sessionId和ai结果
    public Tuple2<String,String> chatWithMemory(AiChatRequest request){
        var sessionId = request.getSessionId();
        var isFirst = false;
        if(StringUtils.isBlank(sessionId)) {
            sessionId = UuidUtil.genUuid();
            isFirst = true;
        }
        var chatMemory = getChatMemory(sessionId);
        if(isFirst)
            chatMemory.add( SystemMessage.from(DEFAULT_SYSTEM_PROMPT));
        chatMemory.add(genUserMsg(request.getImgUrls(),request.getText()));
        var useVlModel = CollectionUtil.isNotEmpty(request.getImgUrls());
        var chatRequest = genChatRequest(chatMemory,useVlModel);
        var chatResp =  chatModel.chat(chatRequest);
        var aiMsg = chatResp.aiMessage();
        chatMemory.add(aiMsg);
        return Tuple.of(sessionId, aiMsg.text());
    }